package com.qd.system.sms;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.cdqidi.dto.ApiResult;
import com.cdqidi.exception.ApiException;
import com.cdqidi.util.MobileUtil;
import com.cdqidi.util.RandUtil;
import com.cdqidi.util.RedisTemplateUtil;
import com.qd.common.panda.constant.PandaConstant;
import com.qd.common.sys.config.SmsConfig;
import com.qd.common.sys.domain.sendmsg.FusionMsg;
import com.qd.common.sys.domain.sendmsg.MsgReceiver;
import com.qd.system.service.msg.client.FusionMsgClientService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author sjk
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class SmsService {
    public final static String PREFIX_ID = "mobile_verification_code_{0}";
    public final static String PREFIX_MOBILE = "mobile_vCode_{0}";
    private final static String PREFIX_LOCKED_ID = "mobile_verification_code_locked_{0}";

    public final static String COUNT_MIN = "mobiles_min";
    private final static String COUNT_MIN_LOCKED_ID = "mobiles_min_locked";

    public final static String COUNT_HOUR = "mobiles_hour";
    private final static String COUNT_HOUR_LOCKED_ID = "mobiles_hour_locked";

    public final static String COUNT_DAY = "mobiles_day";
    private final static String COUNT_DAY_LOCKED_ID = "mobiles_day_locked";

    @Resource
    private SmsConfig smsConfig;
    @Resource
    private RedisTemplate<String, Integer> redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private final FusionMsgClientService fusionMsgClientService;
    private final RedissonClient redissonClient;


    /**
     * 判断验证码是否正确
     *
     * @param mobile 手机号
     * @param id     从缓存里面取出系统生成验证码的id
     * @param vCode  用户输入验证码
     * @return 成功返回true，失败抛出异常信息
     */
    public boolean checkVerificationCode(String mobile, String id, String vCode) {
        if (vCode.length() != smsConfig.getVCodeLength()) {
            log.info("验证码位数不正确，用户输入: {},系统验证码长度: {}", vCode, smsConfig.getVCodeLength());
            throw new ApiException(20004, "验证码输入错误，请重新输入");
        }
        if (!StringUtils.hasLength(id)) {
            throw new ApiException(20005, "请获取验证码");
        }
        final String cacheCode = stringRedisTemplate.opsForValue().get(RedisTemplateUtil.buildKey(id,true));
        if (!StringUtils.hasLength(cacheCode)) {
            throw new ApiException(20003, "验证码失效，请重新获取");
        }
        if (vCode.equals(cacheCode)) {
            return true;
        } else {
            log.info("验证码输入错误，用户输入vCode: {},缓存结果: {},手机号: {}", vCode, cacheCode, mobile);
            throw new ApiException(20004, "验证码输入错误，请重新输入");
        }
    }

    @Transactional(readOnly = true)
    public String sendVerificationCode(Object mobileObj) throws ApiException {
        return sendVerificationCode(mobileObj, null, null);
    }

    /**
     * 发送验证码短信，有时效时间和每天针对每个手机号码的限制，以及同一时段内验证码发送的最大次数
     *
     * @param mobileObj  手机号
     * @param contentObj 短信内容，不填用默认的
     * @param signObj    签名，不填用默认的
     * @return 发送成功以后生成一个唯一编号返回给客户端，用来验证输入验证码是否正确时使用
     * @throws ApiException 异常
     */
    @Transactional(readOnly = true)
    public String sendVerificationCode(Object mobileObj, Object contentObj, Object signObj) throws ApiException {
        if (null == mobileObj) {
            throw new ApiException(6, "手机号不能为空");
        }
        String mobile = String.valueOf(mobileObj);
        String s = stringRedisTemplate.opsForValue().get(mobile);
        if (StringUtils.hasLength(s)) {
            Long expire = stringRedisTemplate.getExpire(mobile);
            if (null != expire && expire > 0) {
                log.error("请稍后在获取验证码，失效时间还剩： {}s,手机号: {}", expire, mobile);
                throw new ApiException(6, "请稍后在获取验证码");
            }
        }
        String countMinKey = RedisTemplateUtil.buildKey(COUNT_MIN, true);
        final Integer cacheMinCount = getCacheCount(countMinKey, COUNT_MIN_LOCKED_ID);
        if (null != cacheMinCount && (cacheMinCount > (smsConfig.getVCodeMaxMinutes() - 1))) {
            throw new ApiException(20000, "超过最大发送上线，请过几分钟后在试");
        }
        String countHourKey = RedisTemplateUtil.buildKey(COUNT_HOUR, true);
        final Integer cacheHourCount = getCacheCount(countHourKey, COUNT_HOUR_LOCKED_ID);
        if (null != cacheHourCount && (cacheHourCount > (smsConfig.getVCodeMaxHour() - 1))) {
            throw new ApiException(20000, "超过最大发送上线，请过一个小时后在试");
        }
        String countDayKey = RedisTemplateUtil.buildKey(COUNT_DAY, true);
        final Integer cacheDayCount = getCacheCount(countDayKey, COUNT_DAY_LOCKED_ID);
        if (null != cacheDayCount && (cacheDayCount > (smsConfig.getVCodeMaxDay() - 1))) {
            throw new ApiException(20000, "超过最大发送上线，请过明天再试");
        }
        String lockedId = RedisTemplateUtil.getRedisCacheKey(PREFIX_LOCKED_ID, mobile);
        String key = RedisTemplateUtil.getRedisCacheKey(PREFIX_ID, mobile);
        final Integer mobileCacheCount = getCacheCount(key, lockedId);
        if (null != mobileCacheCount && (mobileCacheCount > (smsConfig.getMobileVcodeCount() - 1))) {
            log.info("手机号超过最大发送条数限制，已发条数： {},最大条数: {},手机号: {}", mobileCacheCount, smsConfig.getMobileVcodeCount(), mobile);
            throw new ApiException(20001, "当前手机号验证码短信超过最大次数限制，24小时允许最大发送量为["+smsConfig.getMobileVcodeCount()+"条]");
        }
        String content = null;
        if (null != contentObj) {
            content = String.valueOf(contentObj);
        }
        String sign = null;
        if (null != signObj) {
            sign = String.valueOf(signObj);
        }
        return sendVerCode(mobile, content, sign);
    }


    /**
     * 发送验证码日志
     *
     * @param mobile 手机号
     * @return 验证码在redis存储中对应的key值
     */
    private String sendVerCode(String mobile, String content, String sign) {
        String lockedId = RedisTemplateUtil.getRedisCacheKey(PREFIX_LOCKED_ID, mobile);
        String key = RedisTemplateUtil.getRedisCacheKey(PREFIX_ID, mobile);
        String vCodeKey = RedisTemplateUtil.getRedisCacheKey(PREFIX_MOBILE, mobile);
        String vCode = stringRedisTemplate.opsForValue().get(vCodeKey);
        long vCodeTimeOut = smsConfig.getVCodeTimeOut();
        if (StringUtils.hasLength(vCode)) {
            log.info("手机号: {},验证码： {},剩余有效期: {}s,验证码失效时间： {}分钟", mobile, vCode, stringRedisTemplate.getExpire(vCodeKey), vCodeTimeOut);
        } else {
            /*
             * 验证码的有效期从配置文件中读取，保证有效期内的验证码都是同一个
             */
            vCode = RandUtil.getRandNum(smsConfig.getVCodeLength());
            stringRedisTemplate.opsForValue().set(vCodeKey, vCode, vCodeTimeOut, TimeUnit.MINUTES);
        }
        boolean b;
        if (!StringUtils.hasLength(sign)) {
            sign = smsConfig.getSign();
        }
//        b = true;
        if (StringUtils.hasLength(content)) {
            b = sendTemplateSms(mobile, content, sign);
        } else {
            b = sendTemplateSms(mobile, vCode, sign);
        }
        if (b) {
            /*
             * 避免同一个手机号码在短时间内重复请求
             */
            stringRedisTemplate.opsForValue().set(mobile, "1", 50, TimeUnit.SECONDS);
            /*
                针对当前号码的总数加1,按天失效
             */
            operatorCacheCount(key, lockedId, 24L, TimeUnit.HOURS);
            /*
                针对所有的总数加1，按分钟失效
             */
            String countMinKey = RedisTemplateUtil.buildKey(COUNT_MIN, true);
            operatorCacheCount(countMinKey, COUNT_MIN_LOCKED_ID, 1L, TimeUnit.MINUTES);
            /*
                针对所有的总数加1，按小时失效
             */
            String countHourKey = RedisTemplateUtil.buildKey(COUNT_HOUR, true);
            operatorCacheCount(countHourKey, COUNT_HOUR_LOCKED_ID, 1L, TimeUnit.HOURS);
            /*
                针对所有的总数加1，按天失效
             */
            String countDayKey = RedisTemplateUtil.buildKey(COUNT_DAY, true);
            operatorCacheCount(countDayKey, COUNT_DAY_LOCKED_ID, 1L, TimeUnit.DAYS);
            String uuid = IdWorker.getIdStr();
            stringRedisTemplate.opsForValue().set(RedisTemplateUtil.buildKey(uuid,true), vCode, vCodeTimeOut, TimeUnit.MINUTES);
            log.info("vCodeId: {}",uuid);
            return uuid;
        }
        throw new ApiException(20002, "发送验证码短信失败");
    }

    private void operatorCacheCount(String key, String lockedId, Long timeout, TimeUnit t) {
        RLock lock = redissonClient.getLock(lockedId);
        try {
            boolean res = lock.tryLock(30, 10, TimeUnit.SECONDS);
            if (res) {
                try {
                    Integer i = redisTemplate.opsForValue().get(key);
                    if (null == i) {
                        i = 0;
                    }
                    i++;
                    if (i == 1) {
                        redisTemplate.opsForValue().set(key, i, timeout, t);
                    } else {
                        Long expire = redisTemplate.getExpire(key);
                        if (null == expire) {
                            expire = 0L;
                        }
                        redisTemplate.opsForValue().set(key, i, expire, TimeUnit.SECONDS);
                    }
                } finally {
                    lock.unlock();
                }
            } else {
                throw new ApiException(ApiResult.error());
            }
        } catch (InterruptedException e) {
            log.error("获得redis锁超时", e);
            throw new ApiException(ApiResult.error());
        }
    }

    private Integer getCacheCount(String key, String lockedId) {
        RLock lock = redissonClient.getLock(lockedId);
        try {
            boolean res = lock.tryLock(30, 10, TimeUnit.SECONDS);
            if (res) {
                try {
                    return redisTemplate.opsForValue().get(key);
                } finally {
                    lock.unlock();
                }
            } else {
                throw new ApiException(ApiResult.error());
            }
        } catch (InterruptedException e) {
            log.error("获得redis锁超时", e);
            throw new ApiException(ApiResult.error());
        }
    }

//    public boolean checkVerCode(String mobile, String verCode, String verCodeId) {
//        if (!verCode.equals(PandaConstant.VER_CODE)) {
//            /*
//             * 检查验证码正确定
//             */
//            return checkVerificationCode(mobile, verCodeId, verCode);
//        }
//        return true;
//    }

    private final static int MOBILE_LENGTH = 11;

    public String getMobile(String mobile) {
        if (!StringUtils.hasLength(mobile)) {
            throw new ApiException("手机号为空，数据异常");
        }
        if (mobile.length() != MOBILE_LENGTH) {
            throw new ApiException("手机号数据异常");
        }
        return MobileUtil.getMobile(mobile);
    }

    public boolean sendTemplateSms(String mobile, String verCode, String signName) {
        try {
            MsgReceiver receiver = new MsgReceiver();
            //接收者类型
            receiver.setClassify(MsgReceiver.CLASSIFY_USER);
            receiver.getProperties().addProp("code", verCode);
            fusionMsgClientService.sendMsg(PandaConstant.T_2001, "验证码短信", PandaConstant.MSG_EVENT_MSG_CLASSIFY_2, mobile, verCode,signName, receiver, FusionMsg.SEND_FLAG_SMS);
        } catch (Exception e) {
            log.error("调用消息发送接口异常", e);
            return false;
        }
        return true;
    }
}
